﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using SlimDX.Direct3D10;
using SlimDX.DXGI;
using SlimDX;
using SlimDX.Windows;
using SlimDX.D3DCompiler;
using System.Runtime.InteropServices;
using System.Windows.Media.Imaging;
using System.IO;

namespace EikonLeap
{
    /// <summary>
    /// Deferred Renderer Implementation
    /// </summary>
    public class Renderer: IDisposable
    {
        struct VSConstants
        {
            public Matrix WorldViewProjection;
        }

        public Renderer(Form wndForm)
        {
            m_wndForm = wndForm;
            Initialize();
        }

        ~Renderer()
        {
            Uninitialize();
            Dispose();
        }

        protected void Initialize()
        {
            SlimDX.DXGI.Factory gfactory = new Factory();
            Adapter adapter = gfactory.GetAdapter(0);
            SlimDX.Direct3D10.Device.CreateWithSwapChain(adapter, DriverType.Hardware,
                                        DeviceCreationFlags.None,
                                        new SlimDX.DXGI.SwapChainDescription()
                                        {
                                            BufferCount = 1,
                                            Flags = SlimDX.DXGI.SwapChainFlags.AllowModeSwitch,
                                            IsWindowed = true,
                                            ModeDescription = new ModeDescription(m_viewport.Width, m_viewport.Height, new SlimDX.Rational(60, 1), Format.R8G8B8A8_UNorm),
                                            OutputHandle = m_wndForm.Handle,
                                            SampleDescription = new SampleDescription(1, 0),
                                            SwapEffect = SwapEffect.Discard,
                                            Usage = Usage.RenderTargetOutput
                                        },
                                        out m_device, out  m_swapChain);

            //Handle Alt + Enter
            using (var factory = m_swapChain.GetParent<Factory>())
            {
                factory.SetWindowAssociation(m_wndForm.Handle, WindowAssociationFlags.IgnoreAltEnter);
            }
            m_wndForm.KeyDown += (o, e) =>
            {
                if (e.Alt && e.KeyCode == Keys.Enter)
                {
                    m_swapChain.IsFullScreen = !m_swapChain.IsFullScreen;
                }
            };

            SetViewport(m_wndForm.ClientSize.Width, m_wndForm.ClientSize.Height);

            //Primary RenderTarget
            using (Texture2D resource = SlimDX.Direct3D10.Resource.FromSwapChain<Texture2D>(m_swapChain, 0))
            {
                m_renderTarget = new RenderTargetView(m_device, resource);
            }

            Texture2DDescription depthTextureDesc = new Texture2DDescription()
            {
                ArraySize = 1,
                BindFlags = SlimDX.Direct3D10.BindFlags.DepthStencil,
                CpuAccessFlags = SlimDX.Direct3D10.CpuAccessFlags.None,
                Format = SlimDX.DXGI.Format.D32_Float,
                Height = m_viewport.Height,
                Width = m_viewport.Width,
                Usage= ResourceUsage.Default,
                MipLevels = 1,
                OptionFlags = ResourceOptionFlags.None,
                SampleDescription = new SampleDescription(1,0),
            };

            Texture2D depthBuffer = new Texture2D(m_device, depthTextureDesc);
            m_depthView = new DepthStencilView(m_device, depthBuffer);
            DepthStencilStateDescription depthDesc = new DepthStencilStateDescription()
            {
                IsDepthEnabled = true,
                IsStencilEnabled = false,
                DepthWriteMask = SlimDX.Direct3D10.DepthWriteMask.All,
                DepthComparison = Comparison.Less
            };
            m_device.OutputMerger.DepthStencilState = DepthStencilState.FromDescription(m_device, depthDesc);
            m_device.OutputMerger.SetTargets(m_depthView, m_renderTarget);

            //Render states    
            RasterizerStateDescription stateDesc = new RasterizerStateDescription();
            stateDesc.CullMode = CullMode.None;
            stateDesc.FillMode = FillMode.Solid;
            m_device.Rasterizer.State = RasterizerState.FromDescription(m_device, stateDesc);
            CreateShaders();
        }

        public void OnResize(int width, int height)
        {
            m_renderTarget.Dispose();
            m_swapChain.ResizeBuffers(1, 0, 0, Format.R8G8B8A8_UNorm, SwapChainFlags.AllowModeSwitch);
            using (var resource = SlimDX.Direct3D10.Resource.FromSwapChain<Texture2D>(m_swapChain, 0))
                m_renderTarget = new RenderTargetView(m_device, resource);

            m_device.OutputMerger.SetTargets(m_renderTarget);
        }


        protected void Uninitialize()
        {
            
        }

        public void Dispose()
        {
            m_renderTarget.Dispose();
            m_swapChain.Dispose();
            m_device.Dispose();
            GC.SuppressFinalize(this);
        }

        public void CreateShaders()
        {
            string error;
            ShaderBytecode vsbc = ShaderBytecode.CompileFromFile("SimpleShaders.vs", "VShader", "vs_4_0", ShaderFlags.None, EffectFlags.None, null, null, out error);
            ShaderBytecode psbc = ShaderBytecode.CompileFromFile("SimpleShader.ps", "PShader", "ps_4_0", ShaderFlags.None, EffectFlags.None);

            VertexShader vs = new VertexShader(m_device, vsbc);
            PixelShader ps = new PixelShader(m_device, psbc);
            m_device.VertexShader.Set(vs);
            m_device.PixelShader.Set(ps);
            var vshaderSignature = ShaderSignature.GetInputSignature(vsbc);

            InputElement pos = new InputElement("POSITION", 0, Format.R32G32B32_Float, 0, 0);
            InputElement tex = new InputElement("TEXCOORD", 0, Format.R32G32_Float, 12, 0);
            InputElement normal = new InputElement("NORMAL", 0, Format.R32G32B32_Float, 20, 0);
            InputLayout layout = new InputLayout(m_device, vshaderSignature, new InputElement[] { pos, tex, normal });
            m_device.InputAssembler.SetInputLayout(layout);
            m_device.InputAssembler.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip);

            //Constant buffer
            BufferDescription cbufferDesc = new BufferDescription(Marshal.SizeOf(typeof(VSConstants)), ResourceUsage.Dynamic, BindFlags.ConstantBuffer, CpuAccessFlags.Write, ResourceOptionFlags.None);
            m_vsConstantBuffer = new SlimDX.Direct3D10.Buffer(m_device, cbufferDesc);
            BufferDescription pscbufferDesc = new BufferDescription(sizeof(bool), ResourceUsage.Dynamic, BindFlags.ConstantBuffer, CpuAccessFlags.Write, ResourceOptionFlags.None);
            m_psConstantBuffer = new SlimDX.Direct3D10.Buffer(m_device, cbufferDesc);
        }

        public Ray GetRayFromScreenPos(float x, float y, Matrix WVP)
        {
            Vector3 p1 = new Vector3(x, y, m_viewport.MinZ);
            Vector3 p2 = new Vector3(x, y, m_viewport.MaxZ);
            Vector3 rayPoint1 = Vector3.Unproject(p1, m_viewport.X, m_viewport.Y, m_viewport.Width, m_viewport.Height, m_viewport.MinZ, m_viewport.MaxZ, WVP);
            Vector3 rayPoint2 = Vector3.Unproject(p2, m_viewport.X, m_viewport.Y, m_viewport.Width, m_viewport.Height, m_viewport.MinZ, m_viewport.MaxZ, WVP);
            p2.Normalize();
            return new Ray(rayPoint1, rayPoint2);
        }

        public void Render(Scene scene)
        {
            scene.Renderer = this;
            m_device.ClearRenderTargetView(m_renderTarget, new Color4(0.0f, 0.0f, 0.0f));
            m_device.ClearDepthStencilView(m_depthView, DepthStencilClearFlags.Depth, 1.0f, 0);
            Matrix sceneTransform = scene.WorldTransform;
            Camera camera = scene.ActiveCamera;
            foreach (var kvp in scene.Objects)
            {
                SceneObject obj = kvp.Value;
                if (m_vertexBuffers.ContainsKey(obj.ObjectID) == false)
                {
                    CreateBuffers(obj);
                }

                //Vertex
                var vertexBuffer = m_vertexBuffers[obj.ObjectID];
                m_device.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(vertexBuffer, 32, 0));

                //Texture
                Matrix matWidthScaling = Matrix.Identity;
                if (m_texures[obj.ObjectID].Count > 0)
                {
                    Texture2D texture = m_texures[obj.ObjectID].First();
                    //Update texture
                    texture = obj.GetTextureData();
                    if (texture != null)
                    {
                        ShaderResourceView rv = new ShaderResourceView(m_device, texture);
                        m_device.PixelShader.SetShaderResource(rv, 0);
                    }
                }

                //Constants
                //Transformations
                Matrix worldTransform = obj.GetLocalTransform() * sceneTransform;
                obj.WVP = worldTransform * camera.ViewTransform * m_projectionTransform; 
                DataStream stream = m_vsConstantBuffer.Map(MapMode.WriteDiscard, SlimDX.Direct3D10.MapFlags.None);
                VSConstants vsConst = new VSConstants();
                vsConst.WorldViewProjection = worldTransform * camera.ViewTransform * m_projectionTransform;
                stream.Write(vsConst);
                stream.Position = 0;
                m_vsConstantBuffer.Unmap();
                stream = m_psConstantBuffer.Map(MapMode.WriteDiscard, SlimDX.Direct3D10.MapFlags.None);
                stream.Write(obj.IsHighlighten);
                stream.Position = 0;
                m_psConstantBuffer.Unmap();
                m_device.VertexShader.SetConstantBuffer(m_vsConstantBuffer, 0);
                m_device.PixelShader.SetConstantBuffer(m_psConstantBuffer, 0);

                //Draw
                m_device.Draw(obj.Vertices.Length, 0);
            }
            m_swapChain.Present(0, PresentFlags.None);
        }

        protected void CreateBuffers(SceneObject obj)
        {
            //Vertex Buffer
            int vertexSize = Marshal.SizeOf(typeof(Vector3)) +
                             Marshal.SizeOf(typeof(Vector2)) +
                             Marshal.SizeOf(typeof(Vector3));
            BufferDescription bufferDesc = new BufferDescription(vertexSize * obj.Vertices.Length, ResourceUsage.Default, BindFlags.VertexBuffer, CpuAccessFlags.None, ResourceOptionFlags.None);
            using (DataStream vertices = new DataStream(vertexSize * obj.Vertices.Length, true, true))
            {
                for (int i = 0; i < obj.Vertices.Length; ++i)
                {
                    vertices.Write(obj.Vertices[i].X);
                    vertices.Write(obj.Vertices[i].Y);
                    vertices.Write(obj.Vertices[i].Z);
                    vertices.Write(obj.TexCoord0[i].X);
                    vertices.Write(obj.TexCoord0[i].Y);
                    vertices.Write(obj.Normals[i].Z);
                    vertices.Write(obj.Normals[i].Z);
                    vertices.Write(obj.Normals[i].Z);
                }
                vertices.Position = 0;
                SlimDX.Direct3D10.Buffer buffer = new SlimDX.Direct3D10.Buffer(m_device, vertices, bufferDesc);
                m_vertexBuffers[obj.ObjectID] = buffer;
            }

            Texture2D texture = obj.GetTextureData();
            m_texures.Add(obj.ObjectID, new List<Texture2D> { texture });
        }

        public void SetViewport(int width, int height)
        {
            m_viewport = new Viewport(0, 0, width, height);
            m_device.Rasterizer.SetViewports(m_viewport);
            RecomputeProjectionMatrix();
        }

        private void RecomputeProjectionMatrix()
        {
            m_projectionTransform = Matrix.PerspectiveFovRH((float)Math.PI / 6.0f, (float)m_viewport.Width / (float)m_viewport.Height, 0.1f, 1000.0f);
        }

        private Matrix m_projectionTransform;
        private Form m_wndForm;
        private Viewport m_viewport;
        private RenderTargetView m_renderTarget;
        private DepthStencilView m_depthView;
        public SlimDX.Direct3D10.Device m_device;
        private SwapChain m_swapChain;
        private Dictionary<uint, SlimDX.Direct3D10.Buffer> m_vertexBuffers = new Dictionary<uint,SlimDX.Direct3D10.Buffer>();
        private Dictionary<uint, List<SlimDX.Direct3D10.Texture2D>> m_texures = new Dictionary<uint, List<Texture2D>>();
        SlimDX.Direct3D10.Buffer m_vsConstantBuffer, m_psConstantBuffer;
    }
}
